#include "StdAfx.h"
#include "TerrainMesh.h"
#include "ViewFrustrum.h"
#include "GraphicsController.h"
#include <stdio.h>
#include <stdexcept>
#include <string>


///////////////////////////////////////////////////////////////////////////////

DWORD CTerrainVertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX2;

///////////////////////////////////////////////////////////////////////////////

CTerrainPatchMesh::CTerrainPatchMesh(CGraphicsController& graphicsController, 
                                     UCHAR* m_heightMap,
                                     UINT heightMapWidth, UINT heightMapHeight,
                                     UINT meshWidth, UINT meshHeight,
                                     UINT offsetXOnMap, UINT offsetZOnMap,
                                     D3DXVECTOR3 scale)
      : m_graphicsController(graphicsController),
      m_vertexBuffer(NULL),
      m_indexBuffer(NULL),
      m_facesCount(0),
      m_verticesCount(0),
      m_scale(scale),
      m_heightMapWidth(heightMapWidth),
      m_heightMapHeight(heightMapHeight),
      m_heightMap(m_heightMap)
{
   createBuffers(meshWidth, meshHeight);
   createVertices(meshWidth, meshHeight, offsetXOnMap, offsetZOnMap);  
   createIndices(meshWidth, meshHeight);
}

///////////////////////////////////////////////////////////////////////////////

CTerrainPatchMesh::~CTerrainPatchMesh(void)
{
   if (m_vertexBuffer != NULL)
   {
      m_vertexBuffer->Release();
      m_vertexBuffer = NULL;
   }

   if (m_indexBuffer != NULL)
   {
      m_indexBuffer->Release();
      m_indexBuffer = NULL;
   }

   m_facesCount = 0;
   m_verticesCount = 0;
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainPatchMesh::render(IDirect3DDevice9& d3Device) const
{
   if ((m_vertexBuffer != NULL) && (m_indexBuffer != NULL))
   {
      d3Device.SetStreamSource(0, m_vertexBuffer, 0, sizeof(CTerrainVertex));
      d3Device.SetIndices(m_indexBuffer);

      d3Device.DrawIndexedPrimitive(D3DPT_TRIANGLESTRIP, 0, 0, m_verticesCount, 0, m_facesCount);
   }
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainPatchMesh::createBuffers(UINT width, UINT height)
{
   m_verticesCount = (width + 1) * (height + 1);
   m_facesCount = ((width * height) * 2) + ((height - 1) * 3);

   m_vertexBuffer = m_graphicsController.createVertexBuffer(sizeof(CTerrainVertex) * m_verticesCount,
                                                            D3DUSAGE_WRITEONLY,
                                                            CTerrainVertex::FVF,
                                                            D3DPOOL_MANAGED);
      
   m_indexBuffer = m_graphicsController.createIndexBuffer(sizeof(USHORT) * (m_facesCount + 2),
                                                            D3DUSAGE_WRITEONLY,
                                                            D3DFMT_INDEX16,
                                                            D3DPOOL_MANAGED);
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainPatchMesh::createVertices(UINT width, UINT height,
                                       UINT offsetXOnMap, UINT offsetZOnMap)
{
   CTerrainVertex* pVertices = NULL;
   if (FAILED(m_vertexBuffer->Lock(0, 0, (void**)&pVertices, 0)))
   {
      throw std::logic_error(
         std::string("Cannot lock the vertex buffer"));
   }

   for (UINT z = offsetZOnMap; z <= (height + offsetZOnMap); z++)
   {
      for (UINT x = offsetXOnMap; x <= (width + offsetXOnMap); x++)
      {
         // generate averaged normal
         D3DXVECTOR3 normal;
         normal  = getNormal(x    , z    );
         normal += getNormal(x + 1, z    );
         normal += getNormal(x + 1, z + 1);
         normal += getNormal(x    , z + 1);
         normal /= 4;

         // create vertex
         float height = (float)(m_heightMap[z * m_heightMapWidth + x])  * m_scale.y;

         // calculate texture coordinates
         float mainTexU   = (float)x / (float)(m_heightMapWidth - 1);
         float mainTexV   = (float)z / (float)(m_heightMapHeight - 1);
         float detailTexU = (float)x / 6.0f;
         float detailTexV = (float)z / 6.0f;

         *pVertices = CTerrainVertex((float)x * m_scale.x, 
                              height,
                              (float)z * m_scale.z, 
                              normal,
                              mainTexU, mainTexV,
                              detailTexU, detailTexV);

         // calculate the bounding coordinates
         if (pVertices->m_coords.x < m_boundingBox.min.x) m_boundingBox.min.x = pVertices->m_coords.x;
         if (pVertices->m_coords.x > m_boundingBox.max.x) m_boundingBox.max.x = pVertices->m_coords.x;
         if (pVertices->m_coords.y < m_boundingBox.min.y) m_boundingBox.min.y = pVertices->m_coords.y;
         if (pVertices->m_coords.y > m_boundingBox.max.y) m_boundingBox.max.y = pVertices->m_coords.y;
         if (pVertices->m_coords.z < m_boundingBox.min.z) m_boundingBox.min.z = pVertices->m_coords.z;
         if (pVertices->m_coords.z > m_boundingBox.max.z) m_boundingBox.max.z = pVertices->m_coords.z;

         // go to the next vertex
         *pVertices++;

      }
   }

   m_vertexBuffer->Unlock();
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainPatchMesh::createIndices(UINT width, UINT height)
{
   USHORT* pIndices = NULL;
   if (FAILED(m_indexBuffer->Lock(0, 0, (void**)&pIndices, 0)))
   {
      throw std::logic_error(
         std::string("Cannot lock the index buffer"));
   }

   int widthInVertices = width + 1;

   for (UINT z = 0; z < height; z++)
   {
      bool isForwards = ((z % 2) == 0);
      if (isForwards)
      {
         for (int x = 0; x < widthInVertices; x++)
         {
            if ((x == 0) && (z > 0)) *pIndices++ = (z * widthInVertices + x);

            *pIndices++ = (z * widthInVertices + x);
            *pIndices++ = (z * widthInVertices + x + widthInVertices);
         }
      }
      else
      {
         // backwards
         for (int x = widthInVertices  - 1; x >= 0; x--)
         {
            if (x == (widthInVertices - 1)) *pIndices++ = (z * widthInVertices + x);

            *pIndices++ = (z * widthInVertices + x);
            *pIndices++ = (z * widthInVertices + x + widthInVertices);
         }
      }
   }

   m_indexBuffer->Unlock();
}

///////////////////////////////////////////////////////////////////////////////

D3DXVECTOR3 CTerrainPatchMesh::getNormal(UINT x, UINT z)
{
   float pt1, pt2, pt3;

   if ((x < 0) || (x >= m_heightMapWidth) || (z < 0) || ( z >= m_heightMapHeight))
   {
      return D3DXVECTOR3(0, 1, 0);
   }
   
   ULONG addr = z * m_heightMapWidth + x;
   LONG addX, addZ;
   if (x < (m_heightMapWidth - 1)) addX = 1; else addX = -1;
   if (z < (m_heightMapHeight - 1)) addZ = m_heightMapWidth; else addZ = -(signed)m_heightMapWidth;

   pt1 = (float)m_heightMap[addr] * m_scale.y;
   pt2 = (float)m_heightMap[addr + addX] * m_scale.y;
   pt3 = (float)m_heightMap[addr + addZ] * m_scale.y;

   D3DXVECTOR3 edge1(0, pt3 - pt1, m_scale.z);
   D3DXVECTOR3 edge2(m_scale.x, pt2 - pt1, 0);

   D3DXVECTOR3 normal;
   D3DXVec3Cross(&normal, &edge1, &edge2);
   D3DXVec3Normalize(&normal, &normal);

   return normal;
}

///////////////////////////////////////////////////////////////////////////////

CTerrainMesh::CTerrainMesh(CGraphicsController& graphicsController, 
                           const std::wstring& heightMapFileName,
                           UINT heightMapWidth, UINT heightMapHeight,
                           const std::wstring& terrainTextureFileName,
                           const std::wstring& detailTextureFileName)
      : m_graphicsController(graphicsController),
      m_scale(1, 1, 1),
      m_heightMapWidth(heightMapWidth),
      m_heightMapHeight(heightMapHeight),
      m_heightMap(NULL),
      m_terrainTexture(NULL),
      m_detailTexture(NULL)
{
   m_scale.x = 4.0f * (512 / (m_heightMapWidth - 1));
   m_scale.y = 2.0f;
   m_scale.z = 4.0f * (512 / (m_heightMapHeight - 1));

   loadHeightMap(heightMapFileName);
   generateMesh(16, 16);

   D3DXCreateTextureFromFile(&(m_graphicsController.getD3Device()), terrainTextureFileName.c_str(), &m_terrainTexture);
   D3DXCreateTextureFromFile(&(m_graphicsController.getD3Device()), detailTextureFileName.c_str(), &m_detailTexture);

   ZeroMemory(&(m_terrainMat.Ambient), sizeof(m_terrainMat.Ambient));
   ZeroMemory(&(m_terrainMat.Specular), sizeof(m_terrainMat.Specular));
   ZeroMemory(&(m_terrainMat.Emissive), sizeof(m_terrainMat.Emissive));
   m_terrainMat.Diffuse.r = m_terrainMat.Diffuse.g = m_terrainMat.Diffuse.b = m_terrainMat.Diffuse.a = 1.0f;

   D3DXMatrixIdentity(&m_mtxWorld);

   if (graphicsController.getSimultaneouslyRenderedTexturesCount() >= 2)
   {
      m_renderer = new SinglePassTerrainRenderer();
   }
   else
   {
      m_renderer = new MultiPassTerrainRenderer();
   }
}

///////////////////////////////////////////////////////////////////////////////

CTerrainMesh::~CTerrainMesh(void)
{
   for (std::vector<CTerrainPatchMesh*>::iterator it = m_subMeshes.begin();
      it != m_subMeshes.end(); it++)
   {
      delete *it;
   }
   m_subMeshes.clear();

   if (m_heightMap != NULL)
   {
      delete [] m_heightMap;
   }

   m_terrainTexture->Release();
   m_terrainTexture = NULL;

   m_detailTexture->Release();
   m_detailTexture = NULL;

   m_heightMapWidth = 0;
   m_heightMapHeight = 0;

   delete m_renderer;
   m_renderer = NULL;
}

///////////////////////////////////////////////////////////////////////////////

void SinglePassTerrainRenderer::render(CViewFrustrum& frustrum, 
                                       IDirect3DDevice9& d3Device,
                                       const std::vector<CTerrainPatchMesh*>& subMeshes,
                                       IDirect3DTexture9& terrainTexture,
                                       IDirect3DTexture9& detailTexture) const
{
   d3Device.SetTexture(0, &terrainTexture);
   d3Device.SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);
   d3Device.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE2X);
   d3Device.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
   d3Device.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);

   d3Device.SetTexture(1, &detailTexture);
   d3Device.SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 1);
   d3Device.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_ADDSIGNED);
   d3Device.SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
   d3Device.SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_TEXTURE);

   for (std::vector<CTerrainPatchMesh*>::const_iterator it = subMeshes.begin();
      it != subMeshes.end(); it++)
   {
      const CTerrainPatchMesh* subMesh = *it;

      if (frustrum.isInside(subMesh->getBoundingBox()))
      {
         subMesh->render(d3Device);
      }
   }

   d3Device.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
}

///////////////////////////////////////////////////////////////////////////////

void MultiPassTerrainRenderer::render(CViewFrustrum& frustrum, 
                                      IDirect3DDevice9& d3Device,
                                      const std::vector<CTerrainPatchMesh*>& subMeshes,
                                      IDirect3DTexture9& terrainTexture,
                                      IDirect3DTexture9& detailTexture) const
{
   d3Device.SetTexture(0, &terrainTexture);
   d3Device.SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);
   d3Device.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE2X);
   d3Device.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
   d3Device.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);

   for (std::vector<CTerrainPatchMesh*>::const_iterator it = subMeshes.begin();
      it != subMeshes.end(); it++)
   {
      const CTerrainPatchMesh* subMesh = *it;

      if (frustrum.isInside(subMesh->getBoundingBox()))
      {
         subMesh->render(d3Device);
      }
   }

   d3Device.SetTexture(0, &detailTexture);
   d3Device.SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 1);
   d3Device.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
   d3Device.SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
   d3Device.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR);
   d3Device.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);

   for (std::vector<CTerrainPatchMesh*>::const_iterator it = subMeshes.begin();
      it != subMeshes.end(); it++)
   {
      const CTerrainPatchMesh* subMesh = *it;

      if (frustrum.isInside(subMesh->getBoundingBox()))
      {
         subMesh->render(d3Device);
      }
   }

   d3Device.SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
   d3Device.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
   d3Device.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);
   d3Device.SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainMesh::render(CViewFrustrum& frustrum, IDirect3DDevice9& d3Device)
{
   d3Device.SetFVF(CTerrainVertex::FVF);
   d3Device.SetMaterial(&m_terrainMat);
   d3Device.SetTransform(D3DTS_WORLD, &m_mtxWorld);

   m_renderer->render(frustrum, d3Device, m_subMeshes, *m_terrainTexture, *m_detailTexture);
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainMesh::loadHeightMap(const std::wstring& heightMapFileName)
{
   m_heightMap = new UCHAR[m_heightMapWidth * m_heightMapHeight];

   FILE *file = NULL;
   _wfopen_s(&file, heightMapFileName.c_str(), _T("rb"));
   if (file == NULL)
   {
      throw std::invalid_argument(
         std::string("Can't open file for reading"));
   }

   // Read the heightmap data (grayscale)
   fread(m_heightMap, m_heightMapWidth * m_heightMapHeight, 1, file);
   fclose(file);
}

///////////////////////////////////////////////////////////////////////////////

void CTerrainMesh::generateMesh(USHORT subMeshWidthInQuads, USHORT subMeshHeightInQuads)
{
   UINT horizontalNumSubMeshes = m_heightMapWidth  / subMeshWidthInQuads;
   UINT verticalNumSubMeshes   = m_heightMapHeight / subMeshHeightInQuads;

   for (UINT z = 0; z < verticalNumSubMeshes; z++)
   {
      for (UINT x = 0; x < horizontalNumSubMeshes; x++)
      {
         CTerrainPatchMesh* subMesh = new CTerrainPatchMesh(m_graphicsController, 
                                                m_heightMap, m_heightMapWidth, m_heightMapHeight,
                                                subMeshWidthInQuads, subMeshHeightInQuads,
                                                x * subMeshWidthInQuads, z * subMeshHeightInQuads,
                                                m_scale);

         if (subMesh == NULL)
         {
            throw std::logic_error(
               std::string("Cannot create a submesh"));
         }

         // calculate the bounding coordinates
         AABoundingBox subMeshAABB = subMesh->getBoundingBox();
         if (subMeshAABB.min.x < m_boundingBox.min.x) m_boundingBox.min.x = subMeshAABB.min.x;
         if (subMeshAABB.max.x > m_boundingBox.max.x) m_boundingBox.max.x = subMeshAABB.max.x;
         if (subMeshAABB.min.y < m_boundingBox.min.y) m_boundingBox.min.y = subMeshAABB.min.y;
         if (subMeshAABB.max.y > m_boundingBox.max.y) m_boundingBox.max.y = subMeshAABB.max.y;
         if (subMeshAABB.min.z < m_boundingBox.min.z) m_boundingBox.min.z = subMeshAABB.min.z;
         if (subMeshAABB.max.z > m_boundingBox.max.z) m_boundingBox.max.z = subMeshAABB.max.z;

         m_subMeshes.push_back(subMesh);
      }
   }
}

///////////////////////////////////////////////////////////////////////////////

D3DXVECTOR3 CTerrainMesh::getIntersectionRemovalVector(const AABoundingBox& boundingBox) const
{
   float terrainHeight = getTerrainHeight(boundingBox.min);

   if (terrainHeight > boundingBox.min.y)
   {
      return D3DXVECTOR3(0, terrainHeight - boundingBox.min.y, 0);
   }
   else
   {
      return D3DXVECTOR3(0, 0, 0);
   }
}

///////////////////////////////////////////////////////////////////////////////
#define IS_ODD(num) ((num % 2) != 0)

float CTerrainMesh::getTerrainHeight(const D3DXVECTOR3& pos) const
{

   float posX = pos.x / m_scale.x;
   float posZ = pos.z / m_scale.z;
   bool reverseQuad = IS_ODD((UINT)(posZ));

   // there height equals 0 outside the map
   if ((posX < 0)  || (posZ < 0) || (posX >= m_heightMapWidth) || (posZ >= m_heightMapHeight))
   {
      return 0;
   }

   UINT ix = (UINT)posX;
   UINT iz = (UINT)posZ;
   float percentX = posX - ((float)ix);
   float percentZ = posZ - ((float)iz);


   float topLeft, bottomLeft, topRight, bottomRight;
   if (reverseQuad)
   {
      topLeft     = (float) m_heightMap[ix + iz * m_heightMapWidth] * m_scale.y;
      bottomRight = (float) m_heightMap[(ix + 1) + (iz + 1) * m_heightMapWidth] * m_scale.y;

      // which triangle of the quad are we in?
      if (percentX < percentZ)
      {
         bottomLeft = (float) m_heightMap[ix + (iz + 1) * m_heightMapWidth] * m_scale.y;
         topRight   = topLeft + (bottomRight - bottomLeft);
      }
      else
      {
         topRight   = (float) m_heightMap[(ix + 1) + iz * m_heightMapWidth] * m_scale.y;
         bottomLeft = topLeft + (bottomRight - topRight);
      }
   }
   else
   {
      topRight   = (float) m_heightMap[(ix + 1) + iz * m_heightMapWidth] * m_scale.y;
      bottomLeft = (float) m_heightMap[ix + (iz + 1) * m_heightMapWidth] * m_scale.y;

      if (percentX < (1.0f - percentZ))
      {
         topLeft     = (float) m_heightMap[ix + iz * m_heightMapWidth] * m_scale.y;
         bottomRight = bottomLeft + (topRight - topLeft);
      }
      else
      {
         bottomRight = (float) m_heightMap[(ix + 1) + (iz + 1) * m_heightMapWidth] * m_scale.y;
         topLeft     = topRight + (bottomLeft - bottomRight);
      }
   }

   float topHeight    = topLeft    + ((topRight - topLeft) * percentX);
   float bottomHeight = bottomLeft + ((bottomRight - bottomLeft) * percentX);

   return topHeight + ((bottomHeight - topHeight) * percentZ);
}

///////////////////////////////////////////////////////////////////////////////